<template>
  <div ref="scaner" class="scaner">
    <div v-if="showBanner" class="banner">
      <i class="close_icon" @click="() => showBanner = false" />
      <p class="text">若当前浏览器无法扫码，请切换其他浏览器尝试</p>
    </div>
    <div class="cover">
      <p class="line" />
      <span class="square top left" />
      <span class="square top right" />
      <span class="square bottom right" />
      <span class="square bottom left" />
      <p class="tips">将二维码放入框内，即可自动扫描</p>
    </div>
    <video v-show="showPlay" ref="video" class="source" :width="videoWH.width" :height="videoWH.height" controls />
    <canvas v-show="!showPlay" ref="canvas" />
    <button v-show="showPlay" @click="run">开始</button>
  </div>
</template>

<script lang="ts" setup>
import adapter from 'webrtc-adapter'
import jsQR from 'jsqr'
import { computed, nextTick, onMounted, onUnmounted, watch, ref } from 'vue'
const props = defineProps({
  // 使用后置相机
  useBackCamera: {
    type: Boolean,
    default: true
  },
  // 扫描识别后停止
  stopOnScaned: {
    type: Boolean,
    default: true
  },
  drawOnfound: {
    type: Boolean,
    default: true
  },
  // 线条颜色
  lineColor: {
    type: String,
    default: '#03C03C'
  },
  // 线条宽度
  lineWidth: {
    type: Number,
    default: 2
  },
  // 视频宽度
  videoWidth: {
    type: Number,
    default: document.documentElement.clientWidth || document.body.clientWidth
  },
  // 视频高度
  videoHeight: {
    type: Number,
    default: document.documentElement.clientHeight - 48 || document.body.clientHeight - 48
  },
  responsive: {
    type: Boolean,
    default: false
  }
})
const emit = defineEmits(['error-captured', 'code-scanned'])
let showPlay = $ref(false)
let showBanner = $ref(true)
let containerWidth = $ref(null)
let active = $ref(false)
let canvas = $ref(null)
const videoEle = ref(null)
const scanerEle = ref(null)
const video = videoEle.value
const scaner = scanerEle.value
let parity = $ref(0)
let previousCode = $ref(null)
const videoWH = computed(() => {
  if (containerWidth) {
    const width = containerWidth
    const height = width * 0.75
    return { width, height }
  }
  return { width: props.videoWidth, height: props.videoHeight }
})
watch(active, () => {
  if (!active) {
    fullStop()
  }
})

// 画线
const drawLine = (begin, end) => {
  canvas.beginPath()
  canvas.moveTo(begin.x, begin.y)
  canvas.lineTo(end.x, end.y)
  canvas.lineWidth = props.lineWidth
  canvas.strokeStyle = props.lineColor
  canvas.stroke()
}
// 画框
const drawBox = (location) => {
  if (props.drawOnfound) {
    drawLine(location.topLeftCorner, location.topRightCorner)
    drawLine(location.topRightCorner, location.bottomRightCorner)
    drawLine(location.bottomRightCorner, location.bottomLeftCorner)
    drawLine(location.bottomLeftCorner, location.topLeftCorner)
  }
}
const tick = () => {
  if (video && video.readyState === video.HAVE_ENOUGH_DATA) {
    canvas.height = videoWH.value.height
    canvas.width = videoWH.value.width
    canvas.drawImage(video, 0, 0, canvas.width, canvas.height)
    const imageData = canvas.getImageData(0, 0, canvas.width, canvas.height)
    let code: any = false
    try {
      code = jsQR(imageData.data, imageData.width, imageData.height)
    } catch (e) {
      console.error(e)
    }
    if (code) {
      drawBox(code.location)
      found(code.data)
    }
  }
  run()
}
// 初始化
const initData = async () => {
  if (props.responsive) {
    await nextTick()
    containerWidth = scaner.clientWidth
  }
  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    previousCode = null
    parity = 0
    active = true
    canvas = canvas.getContext('2d')
    const facingMode = props.useBackCamera ? { exact: 'environment' } : 'user'
    const handleSuccess = (stream) => {
      if (video.srcObject !== undefined) {
        video.srcObject = stream
      } else if (window.videoEl.mozSrcObject !== undefined) {
        video.mozSrcObject = stream
      } else if (window.URL.createObjectURL) {
        video.src = window.URL.createObjectURL(stream)
      } else if (window.webkitURL) {
        video.src = window.webkitURL.createObjectURL(stream)
      } else {
        video.src = stream
      }
      video.playsInline = true
      const playPromise = video.play()
      playPromise.catch(() => (showPlay = true))
      playPromise.then(run)
    }
    navigator.mediaDevices
      .getUserMedia({
        video: { facingMode }
      })
      .then(handleSuccess)
      .catch(() => {
        navigator.mediaDevices
          .getUserMedia({ video: true })
          .then(handleSuccess)
          .catch((error) => {
            emit('error-captured', error)
          })
      })
  }
}
const run = () => {
  if (active) {
    requestAnimationFrame(tick)
  }
}
const found = (code) => {
  if (previousCode !== code) {
    previousCode = code
  } else if (previousCode === code) {
    parity += 1
  }
  if (parity > 2) {
    active = !stopOnScanned
    parity = 0
    emit('code-scanned', code)
  }
}
// 完全停止
const fullStop = () => {
  if (video && video.srcObject) {
    video.srcObject.getTracks().forEach((t) => t.stop())
  }
  onMounted(() => {
    initData()
  })
  onUnmounted(() => {
    fullStop()
  })
}
</script>

<style lang="css" scoped>
.scaner {
  background: #000000;
  position: fixed;
  top: 48px;
  left: 0;
  width: 100%;
  height: 100%;
  height: -webkit-calc(100% - 48px);
  height: -moz-calc(100% - 48px);
  height: -ms-calc(100% - 48px);
  height: -o-calc(100% - 48px);
  height: calc(100% - 48px);
}
.scaner .banner {
  width: 340px;
  position: absolute;
  top: 16px;
  left: 50%;
  margin-left: -170px;
  background: #fa74a2;
  border-radius: 8px;
  box-sizing: border-box;
  padding: 12px;
  opacity: 0.9;
  box-shadow: 1px 1px 10px rgba(0, 0, 0, 0.2);
}
.scaner .banner .text {
  padding: 0;
  margin: 0;
  color: #ffffff;
  font-size: 12px;
  text-align: justify;
  text-align-last: left;
}
.scaner .banner .close_icon {
  display: inline-block;
  height: 24px;
  width: 24px;
  background: url('../assets/close.png') no-repeat center;
  background-size: auto 100%;
  position: absolute;
  right: 8px;
  top: 8px;
}
.scaner .cover {
  height: 440px;
  width: 440px;
  position: absolute;
  top: 50%;
  left: 50%;
  -webkit-transform: translate(-50%, -50%);
  -moz-transform: translate(-50%, -50%);
  -ms-transform: translate(-50%, -50%);
  -o-transform: translate(-50%, -50%);
  transform: translate(-50%, -50%);
  border: 0.5px solid #999999;
  z-index: 1111;
}
.scaner .cover .line {
  width: 440px;
  height: 2px;
  margin-left: 10px;
  background: #5f68e8;
  background: linear-gradient(to right, transparent, #5f68e8, #0165ff, #5f68e8, transparent);
  position: absolute;
  -webkit-animation: scan 1.75s infinite linear;
  -moz-animation: scan 1.75s infinite linear;
  -ms-animation: scan 1.75s infinite linear;
  -o-animation: scan 1.75s infinite linear;
  animation: scan 1.75s infinite linear;
  -webkit-animation-fill-mode: both;
  -moz-animation-fill-mode: both;
  -ms-animation-fill-mode: both;
  -o-animation-fill-mode: both;
  animation-fill-mode: both;
  border-radius: 1px;
}
.scaner .cover .square {
  display: inline-block;
  height: 20px;
  width: 20px;
  position: absolute;
}
.scaner .cover .square.top {
  top: 0;
  border-top: 1px solid #5f68e8;
}
.scaner .cover .square.left {
  left: 0;
  border-left: 1px solid #5f68e8;
}
.scaner .cover .square.bottom {
  bottom: 0;
  border-bottom: 1px solid #5f68e8;
}
.scaner .cover .square.right {
  right: 0;
  border-right: 1px solid #5f68e8;
}
.scaner .cover .tips {
  position: absolute;
  bottom: -100px;
  width: 100%;
  font-size: 14px;
  color: #ffffff;
  opacity: 0.8;
}
@-webkit-keyframes scan {
  0% {
    top: 0;
  }
  25% {
    top: 100px;
  }
  50% {
    top: 200px;
  }
  75% {
    top: 300px;
  }
  100% {
    top: 400px;
  }
}
@-moz-keyframes scan {
  0% {
    top: 0;
  }
  25% {
    top: 100px;
  }
  50% {
    top: 200px;
  }
  75% {
    top: 300px;
  }
  100% {
    top: 400px;
  }
}
@-o-keyframes scan {
  0% {
    top: 0;
  }
  25% {
    top: 100px;
  }
  50% {
    top: 200px;
  }
  75% {
    top: 300px;
  }
  100% {
    top: 400px;
  }
}
@keyframes scan {
  0% {
    top: 0;
  }
  25% {
    top: 100px;
  }
  50% {
    top: 200px;
  }
  75% {
    top: 300px;
  }
  100% {
    top: 400px;
  }
}
</style>
